import { PackageScriptsConfig, ScriptsConfig } from "@ap/types/translate";
import {
  areFoldersOverlapping,
  fileName,
  forEachFile,
  isFileExists,
} from "@ap/util/fs";
import { getWebpackConfig } from "./webpack.config";
import {
  isAllInstalledInNpm,
  Logger,
  resolveIfNotAbsolue,
  safeReadJSONC,
  sleep,
} from "@ap/util/data";
import {
  showErrorMessage,
  showProgress,
  tscErrorMsg,
  webpackErrorMsg,
} from "@ap/util/vscode/message";
import { OutputChannel, OutputWrapper } from "@ap/util/vscode/output";
import { BASE_CFG_JSON } from "@ap/util/packageJson";
import getFilePath from "@ap/util/vscode/getFilePath";
import AppConfig from "./config";
import * as vscode from "vscode";
import { spawn } from "child_process";
import path from "path";
import fs from "fs/promises";
import fssync from "fs";
import {
  ConfigurationItem,
  ConfigurationKey,
} from "@ap/util/vscode/setInsConfig";
import { CreateStatusBar } from "@ap/init";
import { spawnSync } from "child_process";
/**
 * Replace all instances of a keyword in a string with a new string.
 * @param content The content to search and replace in.
 * @param keyword The keyword to search for.
 * @param replace The function to replace the keyword with.
 * @returns The content with all instances of the keyword replaced.
 * @example
 * const content = "const fs = require('fs');";
 * const newContent = importReplacer(content, "require", path => `./${path}.js`);
 */
function requireReplacer(content: string, replace: (path: string) => string) {
  // 匹配require语句
  const requireRegex = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
  return content.replace(requireRegex, (_, path) => {
    return `require("${replace(path)}")`;
  });
}

function importReplacer(content: string, replace: (path: string) => string) {
  // 匹配import语句
  const importRegex =
    /import\s+((\*\s+as\s+[a-zA-Z_$][a-zA-Z_$0-9]*|[a-zA-Z_$][a-zA-Z_$0-9]*)(\s*,\s*{[^}]+}|\s*,\s*[\s\S]+?)?|{[\s\S]+?})(\s*from\s*('|")[^'"]+\5)/g;
  const matches = content.match(importRegex);
  if (!matches) {
    return content;
  }

  // 更新每个路径
  for (const match of matches) {
    const path = match.match(/('|")[^'"]+\1/);
    if (!path) {
      continue;
    }
    content = content.replace(path[0], `"${replace(path[0].slice(1, -1))}"`);
  }

  const dynamicImportRegex = /import\(\s*['"]([^'"]+)['"]\s*\)(?![^"']*['"])/g;
  const dynamicMatches = content.match(dynamicImportRegex);
  if (dynamicMatches?.length) {
    throw new Error(
      "请不要使用动态导入语句，无法分析路径: " + dynamicMatches[0],
    );
  }

  return content;
}

const JSReplacer = (path: string) =>
  `${path.startsWith("./") ? "" : "./"}${path}.js`;

/**
 * Use this function to solve naming conflicts
 * names will be suffixed with _1, _2, _3, ... (with no extension)
 * @param paths absolute paths
 */
function resolveConflict(
  paths: string[],
  { allowJs }: { allowJs: boolean | undefined },
): Map<string, string> {
  const solved = new Map<string, string>();
  paths.forEach((p) => {
    const parsed = path.parse(p);
    if (parsed.ext === ".ts" || (allowJs && parsed.ext === ".js")) {
      let filename = parsed.name + parsed.ext;
      if (solved.has(filename)) {
        // 如果已经有了这个文件名，就使用别名
        let i = 1,
          fn = `${filename}_${i}${parsed.ext}`;
        while (solved.has(fn)) {
          i++;
          if (i > 1000) {
            throw new Error(
              "Cannot solve naming conflict for more than 1000 files.",
            );
          }
        }
        filename = fn;
      }
      solved.set(filename, p);
    }
  });
  return solved;
}

/**
 * - 编译前准备
 * 1. 创建文件名别名（防止打平目录时文件名冲突）
 *     - 获得每个文件相对于基本目录的相对地址
 *     - 创建别名
 *     - 检查每个文件中的import语句
 *     - 将每个import语句的地址与别名匹配，修改为./${别名}.ts
 *     - ?可能要添加对于动态导入的处理
 *     - 记得提高index和clientIndex的优先级（防止被当成别名）
 * 2. 将修改过import的文件按照别名写入temp
 *
 * - 编译
 *
 * - 编译后补丁
 * 1. 修补所有的require语句
 */
/**
 * @class
 * @classdesc 用于编译和打包ts文件
 * @param {ScriptsConfig} config 配置
 * @example
 * const scripts = Scripts.createScripts({
 *    temp: "./temp",
 *    server: "./server",
 *    client: "./client",
 * });
 */
class Scripts {
  static rootDir = "";
  static setRootDir(dir: string) {
    Scripts.rootDir = dir;
  }
  static Declarations = {
    server: "GameAPI.d.ts",
    client: "ClientAPI.d.ts",
  };
  static async installRequirements() {
    return await this.installPackage(AppConfig.extension.compile.requirements);
  }
  static async installPackage(name: string | string[]) {
    const rootDir = path.resolve(Scripts.rootDir);
    const output = new OutputWrapper(
      OutputChannel.getInstance(),
      "Requirements Installer",
    );

    if (!isFileExists(path.resolve(rootDir, "package.json"))) {
      output.warn("package.json 不存在，正在初始化");
      const initProcess = spawn("npm", ["init", "-y"], {
        cwd: rootDir,
        shell: true,
      });
      initProcess.stdout.on("data", (data) => {
        output.info(data.toString());
        console.log(`stdout: ${data}`);
      });
      initProcess.stderr.on("data", (data) => {
        output.error(data.toString());
        console.error(`stderr: ${data}`);
      });
      initProcess.on("error", (err) => {
        output.error(err.toString());
        console.error(`Failed to start subprocess: ${err}`);
      });
      await new Promise<void>((resolve, reject) => {
        initProcess.on("close", (code) => {
          if (code === 0) {
            output.success(`初始化成功：${rootDir}/package.json`);
            resolve();
          } else {
            reject(new Error("初始化失败"));
          }
        });
      });
    }

    if (
      await isAllInstalledInNpm(Array.isArray(name) ? name : [name], rootDir)
    ) {
      output.info("Requirements already installed.");
      return Promise.resolve();
    }
    return await new Promise<void>(async (resolve, reject) => {
      const required = Array.isArray(name) ? name : [name];
      console.log("Installing", required.join(", "), "in", rootDir);
      output.info(`Installing ${required.join(", ")} in ${rootDir}`);
      const npmProcess = spawn(
        "npm",
        [
          "--registry=https://mirrors.cloud.tencent.com/npm/",
          "install",
          "-D",
          "--save-dev",
          ...required,
        ],
        { cwd: rootDir, shell: true },
      );

      npmProcess.stdout.on("data", (data) => {
        output.info(data.toString());
        console.log(`stdout: ${data}`);
      });

      npmProcess.stderr.on("data", (data) => {
        output.error(data.toString());
        console.error(`stderr: ${data}`);
      });

      npmProcess.on("error", (err) => {
        output.error(err.toString());
        console.error(`Failed to start subprocess: ${err}`);
        reject(err);
      });

      npmProcess.on("close", (code) => {
        output.info(`child process exited with code ${code}`);
        console.log(`child process exited with code ${code}`);
        if (code === 0) {
          resolve();
        } else {
          reject(new Error("安装失败"));
        }
      });

      npmProcess.on("exit", (code) => {
        output.info(`child process exited with code ${code}`);
        console.log(`child process exited with code ${code}`);
      });
    });
  }
  static createScripts(config: PackageScriptsConfig) {
    // 检查文件夹是否重叠
    if (areFoldersOverlapping(config.client.base, config.server.base)) {
      throw new Error("Client文件夹和Server文件夹需要是不同的文件夹");
    }
    const folderReader = FolderReader.getInstance();
    return {
      server: new Scripts({
        type: "server",
        folderReader,
        development: config.server.development,
        npmPackages: config.server.npmPackages,
        baseDir: config.server.base,
        tempDir: path.resolve(
          config.server.base,
          AppConfig.extension.compile.compileDistDir,
        ),
        entry: path.resolve(config.server.base, config.server.entry),
        output: path.resolve(
          config.server.base,
          AppConfig.extension.compile.compileDistDir,
          config.server.output,
        ),
        webpack: config.server.webpack,
        jsUpdate: config.jsUpdate,
        outputNameList: config.outputNameList,
        esbuild: config.server.esbuild,
      }),
      client: new Scripts({
        type: "client",
        folderReader,
        development: config.client.development,
        npmPackages: config.client.npmPackages,
        baseDir: config.client.base,
        tempDir: path.resolve(
          config.client.base,
          AppConfig.extension.compile.compileDistDir,
        ),
        entry: path.resolve(config.client.base, config.client.entry),
        output: path.resolve(
          config.client.base,
          AppConfig.extension.compile.compileDistDir,
          config.client.output,
        ),
        webpack: config.client.webpack,
        jsUpdate: config.jsUpdate,
        outputNameList: config.outputNameList,
        esbuild: config.client.esbuild,
      }),
    };
  }

  constructor(protected config: ScriptsConfig) {}

  private calculateDist(tsconfigPath: string, tsconfig: any): string {
    const rootDir = path.resolve(
      path.parse(tsconfigPath).dir,
      tsconfig?.compilerOptions?.rootDir || path.parse(tsconfigPath).dir,
    );
    return path.resolve(
      path.resolve(
        path.parse(tsconfigPath).dir,
        tsconfig?.compilerOptions?.outDir || ".",
      ),
      path.relative(rootDir, this.config.baseDir),
    );
  }

  async getDist() {
    const tsconfigPath = path.resolve(this.config.baseDir, "tsconfig.json");
    const tsconfig = await safeReadJSONC(tsconfigPath);
    return this.calculateDist(tsconfigPath, tsconfig);
  }

  public async compile(): Promise<string> {
    if (
      this.config.npmPackages &&
      this.config.npmPackages !== this.config.type
    ) {
      return "";
    }
    const tsconfigPath = path.resolve(this.config.baseDir, "tsconfig.json");

    const tsconfig = await safeReadJSONC(tsconfigPath);
    const dist = this.calculateDist(tsconfigPath, tsconfig);

    const logDir = path.resolve(this.config.baseDir, ".log");
    if (!(await isFileExists(tsconfigPath))) {
      throw new Error(`tsconfig.json 不存在: ${tsconfigPath}`);
    }

    await this.config.folderReader.createDir(logDir);

    const logFile = path.resolve(logDir, Date.now() + "_typescript.log");
    const logger = new Logger("TypeScript Compiler", logFile);
    const output = new OutputWrapper(
      OutputChannel.getInstance(),
      `${this.config.type} TSC`,
    );
    if(this.config.esbuild){
      logger.info("使用esbuild编译，跳过TSC");
      return dist;
    }

    try {
      const tasks: Promise<void>[] = [];

      const tscProcess = spawn(
        "npx",
        ["tsc", "--project", path.parse(tsconfigPath).dir],
        {
          cwd: path.parse(tsconfigPath).dir,
          shell: true,
        },
      );
      let errorMsg = "";
      await new Promise<void>((resolve, reject) => {
        tscProcess.stdout.on("data", (data) => {
          // OutputChannel.getInstance().appendLine(data.toString());
          if (data.toString().includes(": error TS")) {
            errorMsg += data.toString();
            tasks.push(logger.error(data.toString()));
          } else {
            output.info(data.toString());
          }
        });
        tscProcess.on("close", (code) => {
          if (code === 0) {
            resolve();
          } else {
            errorMsg = "tsc 编译失败，退出码: " + code + "\n" + errorMsg;
            reject(new Error(errorMsg));
            tasks.push(logger.error("tsc 编译失败，退出码: " + code));
            tscErrorMsg(logFile);
          }
        });
        tscProcess.stderr.on("data", (data) => {
          output.error(data.toString());
          showErrorMessage(data.toString());
          tasks.push(logger.error(data.toString()));
          errorMsg = "tsc 编译失败，" + data.toString() + "\n" + errorMsg;
          reject(new Error(errorMsg));
        });
      });
      await Promise.all(tasks);
      return dist;
    } catch (error) {
      throw new Error(String(error));
    }
  }
  /**
   * 编译并且打包
   * @param input 入口文件
   * @param output 输出文件
   * @returns 打包后的文件路径
   */
  public async pack(input: string, output: string): Promise<string> {
    if (
      this.config.npmPackages &&
      this.config.npmPackages !== this.config.type
    ) {
      return "";
    }
    if(this.config.esbuild){
      return await this.esbuildPack(input, output);
    }
    // webpack打包
    const isClient = this.config.type === "client";
    const webpackConfig = getWebpackConfig(
      {
        entry: input,
        dist: path.parse(output).dir,
        filename: fileName(output),
        mode: "production",
        ...(isClient ? { libraryTarget: "module" } : {}),
      },
      {
        devtool: "nosources-source-map",
        optimization: {
          minimize: !this.config.development,
        },
        ...(isClient
          ? {
              experiments: {
                outputModule: true,
              },
              output: {
                chunkFormat: "module",
                path: path.parse(output).dir,
                filename: fileName(output),
                libraryTarget: "module",
              },
            }
          : {}),
        cache: {
          type: "filesystem",
          cacheDirectory: path.resolve(this.config.tempDir, ".cache"),
        },
        context: path.resolve(this.config.baseDir),
        module: {
          rules: [
            {
              test: /\.(ts|tsx)$/,
              use: "ts-loader",
              exclude: /node_modules/,
            },
          ],
        },
        resolve: {
          extensions: [".ts", ".js", ".tsx", ".jsx", ".mjs", ".cjs", ".json"],
          ...{
            modules: [
              "node_modules",
              path.resolve(this.config.baseDir, "node_modules"),
            ],
          },
        },
      },
    );

    console.log("Webpack config:", webpackConfig);

    let head: {
      head: string;
      tail: (varName: string, value: string) => string;
    };
    if (this.config.webpack) {
      head = this.getLoader(
        resolveIfNotAbsolue(
          this.config.webpack,
          path.parse(getFilePath(BASE_CFG_JSON) || "").dir,
        ),
      );
    } else {
      head = { head: "", tail: (_: string, value: string) => value };
    }

    // 由于TerserPlugin的问题，需要使用webpack-cli
    await this.config.folderReader.writeFile(
      path.resolve(this.config.tempDir, "webpack.config.js"),
      this.configToString(webpackConfig as any, head.head, head.tail),
    );

    return await this.webpackProcess();
  }

  /**
   * 编译并且打包(Esbuild)
   * @param input 入口文件
   * @param output 输出文件
   * @returns 打包后的文件路径
   */
  public async esbuildPack(input: string, output: string): Promise<string> {
    await Scripts.installPackage(["esbuild"]);
    const tsconfigPath = path.resolve(this.config.baseDir, "tsconfig.json");
    const esbConfig={
      entryPoints: [input],
      outfile: output,
      bundle: true,
      tsconfig: tsconfigPath,
      minify: !this.config.development
    };
    await this.config.folderReader.writeFile(
      path.resolve(this.config.tempDir, "esbuild.mjs"),
      `import * as esbuild from 'esbuild';
console.log("Esbuild started");
const timer=Date.now();
const res=await esbuild.build(${JSON.stringify(esbConfig)});
console.log("Esbuild compiled successfully in"+(Date.now()-timer)+"ms");
console.log(JSON.stringify(res));`,
    );
    // console.log("Esbuild",input,output);
    return new Promise<string>(async (resolve, reject) => {
      showProgress("正在运行Esbuild", async (progress) => {
        const outputWrapper = new OutputWrapper(
          OutputChannel.getInstance(),
          `${this.config.type} Esbuild`,
        );

        progress.report({ message: "正在运行Esbuild" });
        const logDir = path.resolve(this.config.baseDir, ".log");
        const esbProcess = spawn(
          "node",
          [
            path.resolve(this.config.tempDir, "esbuild.mjs")
          ],
          { shell: true, cwd: this.config.tempDir },
        );
        esbProcess.stdout.on("data", (data) => {
          const dataStr = data.toString();
          if (dataStr.includes("ERROR")) {
            const logFile = path.resolve(logDir, Date.now() + "_esbuild.log");
            const logger = new Logger("Esbuild", logFile);
            logger.error(dataStr.replace(/\x1b\[[0-9;]*m/g, ""));
            outputWrapper.error(dataStr);
            webpackErrorMsg(logFile);
            reject(new Error("esbuild 打包失败: " + dataStr));
          } else {
            outputWrapper.info(dataStr);
          }
          console.log(`stdout: ${data}`);
        });
        esbProcess.stderr.on("data", (data) => {
          const dataStr = data.toString();
          outputWrapper.error(dataStr);
          console.error(`stderr: ${data}`);
          reject(new Error("esbuild 打包失败: " + dataStr));
        });
        esbProcess.on("close", async (code) => {
          outputWrapper.info(`child process exited with code ${code}`);
          console.log(`child process exited with code ${code}`);

          const tsconfigPath = path.resolve(
            this.config.baseDir,
            "tsconfig.json",
          );
          const dist = this.calculateDist(
            tsconfigPath,
            await safeReadJSONC(tsconfigPath),
          );

          console.log("Esbuild process finished.", dist);
          resolve(path.resolve(dist, fileName(this.config.output)));
        });
      });
    });
  }
  public async esbuildSetupHmr(input: string, output: string):Promise<string> {
    const tsconfigPath = path.resolve(this.config.baseDir, "tsconfig.json");
    const esbConfig={
      entryPoints: [input],
      outfile: output,
      bundle: true,
      tsconfig: tsconfigPath,
      minify: !this.config.development,
      legalComments: "none",
      plugins: "<plugins>"
    };
    let bundleName=path.basename(output).split(".").slice(0,-1).join(".");
    const plugins:string=`[{
    name: 'notify',
    setup(build) {
      build.onStart(() => {
        lastbuildTime = Date.now();
        console.log('[watch] building');
      });
      
      build.onEnd((result) => {
        if (result.errors.length) {
          console.error('[watch] build failed');
        } else {
          console.log('[watch][buildfinish] build finished in '+(Date.now() - lastbuildTime)+'ms');
          console.log('[javascript module] (name: ${bundleName})')
        }
      });
    }
  }]`;
    await this.config.folderReader.writeFile(
      path.resolve(this.config.tempDir, "esbuild.mjs"),
      `import * as esbuild from 'esbuild';
const esbConfig=${JSON.stringify(esbConfig).replace(`"<plugins>"`, plugins)};
console.log("Esbuild started");
let lastbuildTime = Date.now();
const ctx = await esbuild.context(esbConfig);
await ctx.watch();`,
    );
    return path.resolve(this.config.tempDir, "esbuild.mjs");
  }

  public async setupHmr(input: string, output: string): Promise<string> {
    if (
      this.config.npmPackages &&
      this.config.npmPackages !== this.config.type
    ) {
      return "";
    }
    await FolderReader.getInstance().createDir(this.config.tempDir);
    if(this.config.esbuild){
      return await this.esbuildSetupHmr(input, output);
    }

    const isClient = this.config.type === "client";
    const result: Record<string, string> = {};
    Object.values(this.config.outputNameList ?? {}).forEach((value) => {
      if (isClient) {
        result[value.clientName] = value.clientEntry;
      } else {
        result[value.serverName] = value.serverEntry;
      }
    });
    if (!result || Object.values(result).length === 0) {
      result[path.parse(output).name] = input;
    }
    const webpackConfig = {
      devtool: "nosources-source-map",
      target: "node",
      mode: "production",
      entry: result,
      output: {
        path: path.parse(output).dir,
        filename: "[name].js",
        libraryTarget: isClient ? "module" : "commonjs2",
        chunkFormat: isClient ? "module" : "commonjs",
      },
      resolve: {
        extensions: [".ts", ".js", ".tsx", ".jsx", ".mjs", ".cjs", ".json"],
      },
      module: {
        rules: [
          {
            test: /\.(ts|tsx)$/,
            use: "ts-loader",
            exclude: /node_modules/,
          },
        ],
      },
      optimization: {
        minimize: !this.config.development,
      },
      ...(isClient
        ? {
            experiments: {
              outputModule: true,
            },
          }
        : {}),
      cache: {
        type: "filesystem",
        cacheDirectory: path.resolve(this.config.tempDir, ".cache"),
      },
      devServer: {
        contentBase: path.resolve(this.config.baseDir),
        hot: true,
      },
    };
    console.log(
      "Webpack path:",
      path.resolve(this.config.tempDir, "webpack.config.js"),
    );
    console.log("Webpack config:", webpackConfig);

    let head: {
      head: string;
      tail: (varName: string, value: string) => string;
    };
    if (this.config.webpack) {
      head = this.getLoader(
        resolveIfNotAbsolue(
          this.config.webpack,
          path.parse(getFilePath(BASE_CFG_JSON) || "").dir,
        ),
      );
    } else {
      head = { head: "", tail: (_: string, value: string) => value };
    }

    // 写入webpack配置文件
    await this.config.folderReader.writeFile(
      path.resolve(this.config.tempDir, "webpack.config.js"),
      this.configToString(webpackConfig as any, head.head, head.tail),
    );
    return path.resolve(this.config.tempDir, "webpack.config.js");
  }

  public async startHmrServer(
    packConfigPath: string,
    onUpdated: (file: string, fileName: string, time: number) => Promise<void>,
    onClose: (code: number | null) => void,
  ) {
    if (
      this.config.npmPackages &&
      this.config.npmPackages !== this.config.type
    ) {
      return {
        pid: undefined,
        stop(): boolean {
          return true;
        },
        isPackage: true,
      };
    }
    const outputWrapper = new OutputWrapper(
      OutputChannel.getInstance(),
      "HMR " + this.config.type,
    );
    OutputChannel.getInstance().show();
    const logDir = path.resolve(this.config.baseDir, ".log");
    if (this.config.npmPackages) {
      outputWrapper.warn(
        "当前项目是" +
          this.config.npmPackages +
          "组件库（npm包），最终上传npmjs前需要完整编译一次项目。",
      );
    }
    outputWrapper.info("Checking dependencies...");

    await Scripts.installRequirements();
    if(this.config.esbuild){
      await Scripts.installPackage("esbuild");
    }
    const builder=this.config.esbuild?"esbuild":"webpack";

    await this.config.folderReader.createDir(logDir);
    const isEnableHmrTsErrorPopupLogging = vscode.workspace
      .getConfiguration(ConfigurationItem.NODEJS_TOOL)
      .get<boolean>(ConfigurationKey.ENABLE_HMR_TS_ERROR_POPUP_LOGGING);
    outputWrapper
      .info("Starting HMR " + this.config.type)
      .info(`Use ${builder} config path: ` + packConfigPath)
      .info(
        builder==="webpack"?
        ("Use spawn args: npx webpack --watch --config " + packConfigPath):
        ("Use spawn args: node "+packConfigPath),
      );

    // for some reason, we cannot launch webpack-dev-server
    // use npx webpack --watch instead
    // and we watch files ourselves

    const ac = new AbortController();
    const { signal } = ac;
    const tempDir = this.config.tempDir;
    const packProcess = builder==="webpack"?spawn(
      "npx",
      ["webpack", "--watch", "--config", packConfigPath],
      { shell: true, cwd: tempDir, signal },
    ):spawn(
      "node",[packConfigPath],
      { shell: true, cwd: tempDir, signal }
    );
    const dist = await this.getDist();
    packProcess.stdout.on("data", async (data) => {
      const time = Date.now();
      CreateStatusBar.buildHMRToolObj.text =
        "$(loading~spin) HMR " + this.config.type + "处理中...";
      const dataStr = data.toString();
      outputWrapper.info(dataStr);
      if (isEnableHmrTsErrorPopupLogging) {
        if (dataStr.includes("ERROR")) {
          const logFile = path.resolve(
            logDir,
            Date.now() + "_hmr_typescript.log",
          );
          const logger = new Logger(builder+"-Ts Compiler", logFile);
          logger.error(dataStr.replace(/\x1b\[[0-9;]*m/g, ""));
          tscErrorMsg(logFile);
        }
      }
      console.log(`stdout: ${data}`);
      if (this.config.jsUpdate === undefined || this.config.jsUpdate === true) {
        const line = /\(name: ([^)]+)\)/g;
        let match;
        while ((match = line.exec(dataStr)) !== null) {
          await onUpdated(
            path.resolve(dist, match[1] + ".js"),
            match[1] + ".js",
            time,
          );
        }
      } else {
        outputWrapper.info(
          "The script will not be uploaded to the map temporarily!",
        );
      }
      CreateStatusBar.buildHMRToolObj.text = "$(arena-flash-icon) HMR";
    });
    packProcess.stderr.on("data", (data) => {
      outputWrapper.error(data.toString());
      console.error(`stderr: ${data}`);
    });
    packProcess.on("close", async (code) => {
      outputWrapper.info(`child process exited with code ${code}`);
      console.log(`child process exited with code ${code}`);
      onClose(code);
      ac.abort();
    });

    // const watcher = fssync.watch(this.config.tempDir, { recursive: true, signal });
    // watcher.on('change', (type, filename) => {
    //     if (type === "change" && String(filename).endsWith(name)) {
    //         outputWrapper.info(`File changed: ${filename}`);
    //         onUpdated(path.resolve(this.config.tempDir, String(filename)));
    //     }
    // });

    return {
      pid: packProcess.pid || 0,
      stop(): boolean {
        try {
          if (process.platform === "win32") {
            spawnSync(
              "taskkill",
              ["/pid", String(packProcess.pid), "-t", "-f"],
              {
                stdio: "ignore",
                shell: true,
                cwd: tempDir,
              },
            );
          } else {
            // 对于类Unix系统（Mac、Linux）
            process.kill(packProcess.pid!, "SIGTERM");
            // 给进程一些时间优雅地终止
            setTimeout(() => {
              try {
                // 如果进程仍在运行则强制终止
                process.kill(packProcess.pid!, "SIGKILL");
              } catch (e) {
                // Process already terminated
              }
            }, 1000);
          }
        } catch (error) {
          // 自杀
          packProcess.stdout?.destroy();
          packProcess.stderr?.destroy();
          packProcess.stdin?.destroy();
          packProcess.kill();
        }
        return true;
      },
    };
  }

  getLoader(resolve: string) {
    const alias = {
      config: "__$0",
      deepMerge: "__$1",
      merge: "__$2",
      tryRequire: "__$3",
    };
    const deepMerge = `($0,$1,$3)=>{$3.keys($1).forEach($2=>{if($1[$2] instanceof $3 && $2 in $0){$1[$2]=(${alias.deepMerge}).call(null,$0[$2],$1[$2],$3);}});return $3.assign($0||{},$1);};`;
    const tryRequire = `(()=>{try{return require(${JSON.stringify(resolve.replace(/\\/g, "/"))});}catch(e){console.error("Cannot load file: " + ${JSON.stringify(resolve.replace(/\\/g, "/"))});return {}};})`;

    const loader = [
      `const ${alias.tryRequire} = ${tryRequire};`,
      `const ${alias.config} = (${alias.tryRequire})()`,
      `const ${alias.deepMerge} = ${deepMerge.toString()}`,
      `const ${alias.merge} = ($0, $1) => ${alias.deepMerge}.call(null,$0,$1,Object)`,
    ];
    const tail = (varName: string, value: string) =>
      [
        `${value}`,
        `\n${varName} = ${alias.merge}(${varName}, ${alias.config})`,
      ].join(";");

    return {
      head: loader.join(";"),
      tail: tail,
    };
  }

  async webpackProcess() {
    return new Promise<string>(async (resolve, reject) => {
      showProgress("正在运行Webpack", async (progress) => {
        const outputWrapper = new OutputWrapper(
          OutputChannel.getInstance(),
          `${this.config.type} Webpack`,
        );

        progress.report({ message: "正在运行Webpack" });
        const logDir = path.resolve(this.config.baseDir, ".log");
        const webpackProcess = spawn(
          "npx",
          [
            "webpack",
            "--config",
            path.resolve(this.config.tempDir, "webpack.config.js"),
          ],
          { shell: true, cwd: this.config.tempDir },
        );
        webpackProcess.stdout.on("data", (data) => {
          const dataStr = data.toString();
          if (dataStr.includes("ERROR")) {
            const logFile = path.resolve(logDir, Date.now() + "_webpack.log");
            const logger = new Logger("Webpack", logFile);
            logger.error(dataStr.replace(/\x1b\[[0-9;]*m/g, ""));
            outputWrapper.error(dataStr);
            webpackErrorMsg(logFile);
            reject(new Error("webpack 打包失败: " + dataStr));
          } else {
            outputWrapper.info(dataStr);
          }
          console.log(`stdout: ${data}`);
        });
        webpackProcess.stderr.on("data", (data) => {
          const dataStr = data.toString();
          outputWrapper.error(dataStr);
          console.error(`stderr: ${data}`);
          reject(new Error("webpack 打包失败: " + dataStr));
        });
        webpackProcess.on("close", async (code) => {
          outputWrapper.info(`child process exited with code ${code}`);
          console.log(`child process exited with code ${code}`);

          const tsconfigPath = path.resolve(
            this.config.baseDir,
            "tsconfig.json",
          );
          const dist = this.calculateDist(
            tsconfigPath,
            await safeReadJSONC(tsconfigPath),
          );

          console.log("Webpack process finished.", dist);
          resolve(path.resolve(dist, fileName(this.config.output)));
        });
      });
    });
  }
  async addExtensions(filePaths: string[]) {
    await Promise.all(
      filePaths.map(async (p) => {
        const content = await this.config.folderReader.readFile(p);
        const newContent = requireReplacer(content, (p) =>
          p.startsWith(".") ? JSReplacer(p) : p,
        );
        return await this.config.folderReader.writeFile(p, newContent);
      }),
    );
  }
  public async readDist() {
    return await this.config.folderReader.readRaw(
      path.resolve(this.config.output),
    );
  }
  async readTsConfig(p: string) {
    const content = await this.config.folderReader.readFile(p);
    return safeReadJSONC(content);
  }

  configToString(
    config: Record<string, Object | number | string | String | any[] | RegExp>,
    content?: string,
    handler?: (varName: string, value: string) => string,
  ): string {
    const varName = "__CONFIG__";
    const head = (content || "") + ";\n" + `var ${varName} = `;
    const body = JSON.stringify(
      config,
      (_, value) => {
        if (value instanceof RegExp) {
          return `__REGEXP__${value.source}__FLAGS__${value.flags}`;
        } else if (typeof value === "string") {
          return `__STRING__${value}__STRING__`;
        }
        return value;
      },
      4,
    );

    // 替换标记为字符串和正则表达式的部分
    let finalBody = body
      .replace(/"__STRING__(.*?)__STRING__"/g, (_, p1) => `"${p1}"`)
      .replace(
        /"__REGEXP__(.*?)__FLAGS__(.*?)"/g,
        (_, source, flags) =>
          `new RegExp(${JSON.stringify(source).replace("\\\\", "\\")}, ${JSON.stringify(flags)})`,
      );
    if (handler) {
      finalBody = handler(varName, finalBody);
    }

    return (
      head +
      finalBody +
      `;\n;module.exports = ${varName};console.log("Webpack config loaded.", ${varName});`
    );
  }
}

class FolderReader {
  static instance: FolderReader;
  static getInstance() {
    if (!FolderReader.instance) {
      FolderReader.instance = new FolderReader();
    }
    return FolderReader.instance;
  }
  constructor() {}

  async getFilePaths(dirPath: string): Promise<string[]> {
    return await forEachFile(dirPath);
  }
  async writeFile(filePath: string, data: string): Promise<void> {
    return await fs.writeFile(filePath, data);
  }
  async createDir(dirPath: string): Promise<void> {
    return void (await fs.mkdir(dirPath, { recursive: true }));
  }
  async forEachFile(dirPath: string, cb: (path: string) => any): Promise<void> {
    return void (await forEachFile(dirPath, cb));
  }
  async readFile(filePath: string): Promise<string> {
    return (await fs.readFile(filePath, "utf-8")).toString();
  }
  async cleanDirContents(
    dirPath: string,
    includedFolders: string[],
  ): Promise<void> {
    const files = await fs.readdir(dirPath);
    for (const file of files) {
      const filePath = path.join(dirPath, file);
      const stat = await fs.stat(filePath);
      if (stat.isFile()) {
        await fs.rm(filePath, { force: true });
      } else if (stat.isDirectory() && includedFolders.includes(file)) {
        await fs.rm(filePath, { force: true, recursive: true });
      }
    }
  }
  async readRaw(filePath: string) {
    return await fs.readFile(filePath);
  }
}

export {
  Scripts,
  FolderReader,
  requireReplacer,
  importReplacer,
  JSReplacer,
  resolveConflict,
};
